'''
Created on 2011-06-27

@author: Earl Bell
'''

from Tkinter import *
import OSEKitsDefine 
from math import exp
import time
import tkMessageBox
import datetime

import matplotlib
matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure


class OSEThermocouple(object):
    '''
    Class: OSEThermocouple
    
    Description:
        This class handles the use of the thermocouple sensor.  Contains functions to parse measurement packets, re-draw
        the display, store data to a file, and configure the sensors.
    '''
    
    _measured_temperatures = []
    _measured_times = []
    _start_time = 0
    
    def __init__(self):
        self._sample_rate = StringVar()
        return
    
    def save_data(self, file_type, file):
        '''
        Function: save_data
        
        Description:
            Takes data from last run thermocouple experiment and saves it to either a csv or xml file.
            
        Parameters:
            String file_type: "csv" store data in csv format (tab delimited)
                              "xml" store data in xml format
            File file: an already opened file where the data is to be stored.
        
        Returns:
            String: "success" if no errors encountered.
                    "<error message>" otherwise
        '''
        if file_type == "csv":
            try:
                now = datetime.datetime.now()
                current_time= now.strftime("%H:%M %Y-%m-%d")
                file.write("OSEKits Thermocouple Data\t")
                file.write(current_time)
                file.write("\n")
                file.write("Elapsed Time (s)\tTemperature (degrees C)\n")
                for i in range(0, len(self._measured_times)):
                    file.write(str(self._measured_times[i]) + '\t' + str(self._measured_temperatures[i]) + '\n')
                file.close()
            except Exception, e:
                return "File error: " + str(e)
        elif file_type == "xml":
            try:
                file.write("""<?xml version="1.0" encoding="UTF-8" ?>\n""")
                file.write("<OSEData>\n")
                file.write("<title>OSEKits Thermocouple Data</title>\n")
                now = datetime.datetime.now()
                current_time= now.strftime("%H:%M %Y-%m-%d")
                file.write("<datetime>")
                file.write(current_time)
                file.write("</datetime>\n")
                for i in range(0, len(self._measured_times)):
                    file.write("<measurement>\n")
                    file.write("\t<elapsedtime>" + str(self._measured_times[i]) +"</elapsedtime>\n") 
                    file.write("\t<temperature>" + str(self._measured_temperatures[i]) + '</temperature>\n')
                    file.write("</measurement>\n")
                file.write("</OSEData>")
                file.close()
            except Exception, e:
                return "File error: " + str(e)
                
        return "success"

    def setup_display(self, MainWindow):
        '''
        Function: setup_display
        
        Description:
            Adds all display elements to the main window.
            
        Parameters:
            Tkinter.Tk   The main tk window object where the display will be altered

        Returns:
            Nothing
        '''        
        self._measured_temperatures = []
        self._measured_times = []
        
        
        self.gain_label = Label(MainWindow, text = "Gain:")
        self.gain_label.grid(column = 0, row = OSEKitsDefine.FIRST_DISPLAY_ROW, sticky = W+S, pady=90)
        
        self.gain_value = StringVar()
        self.gain_value.set(str(1000))
        self.gainMenu = OptionMenu(MainWindow, self.gain_value, "100","200","500", "1000")
        self.gainMenu.grid(column = 0, row = OSEKitsDefine.FIRST_DISPLAY_ROW, sticky = W+S, pady = 60 )

        
        self.sample_rate_label = Label(MainWindow, text = "Time between measurements\n(0.25s to 3600s)")
        self.sample_rate_label.pack()
        self.sample_rate_label.grid(column = 0, row=OSEKitsDefine.FIRST_DISPLAY_ROW, sticky = S+W, pady = 20)
        
        self.sample_rate_entry = Spinbox(MainWindow, background = "white", increment = 1, repeatdelay = 200, repeatinterval = 200, from_ = 1, to_ = 3600, textvariable = self._sample_rate)
        self.sample_rate_entry.pack()
        self.sample_rate_entry.grid(column = 0, row = OSEKitsDefine.FIRST_DISPLAY_ROW, sticky = S+W)
        
        self._temperature_figure = Figure(figsize=(8,8), dpi=80)
        self._temperature_plot = self._temperature_figure.add_subplot(111)
        self._temperature_plot.plot(self._measured_times,self._measured_temperatures)
        self._temperature_plot.set_title('Temperature vs Time')
        self._temperature_plot.set_xlabel('Time (s)')
        self._temperature_plot.set_ylabel('Temperature (C)')
        self._temperature_figure.subplots_adjust(top=0.9, bottom = 0.125)

        
        self.temperature_display = FigureCanvasTkAgg(self._temperature_figure, master=MainWindow)
        self.temperature_display.show()
        self.temperature_display.get_tk_widget().grid()
        self.temperature_display._tkcanvas.grid(column = 1, row = OSEKitsDefine.FIRST_DISPLAY_ROW, sticky =S+W, padx = 10)        
        
        return
    
    def get_sample_rate(self):
        '''
        Function: get_sample_rate
        
        Description:
            Returns the value of the sample rate field on the display.
            
        Returns:
            Float
        '''
        try:
            rate_string = self._sample_rate.get()
            if len(rate_string) == 0:
                rate_string = '1.0'
            if rate_string[0] == '.':
                rate_string = '0' + rate_string
            sample_rate = float(rate_string)
        except:
            tkMessageBox.showerror("Entry Error", "The number entered is invalid.\n Must be positive, starting with a number.\n Using default of 10 seconds.")
            self._sample_rate.set('10')
            return 10.0
        
        return sample_rate
    
    def destroy_display(self, MainWindow):
        '''
        Function: destroy_display
        
        Description:
            Removes all display elements from the main window.
            
        Parameters:
            Tkinter.Tk   The main tk window object where the display will be altered
        
        Returns:
            Nothing
        '''        
        self.gain_label.destroy()
        self.gainMenu.destroy()
        self.sample_rate_entry.destroy()
        self.sample_rate_label.destroy()    
        self.temperature_display.get_tk_widget().destroy()
        self.temperature_display._tkcanvas.destroy()
        
        return
    
    def _TC_K_CtoV(self, degrees):
        '''
        Function: _TC_K_CtoV
        
        Description:
            Converts from degrees Celsius to a voltage for a K-type thermocouple.
            
        Returns:
            Float
        '''
        # Coefficients and equations are from NIST:
        #    http://srdata.nist.gov/its90/download/type_k.tab
        # These are for K-type thermocouples --
        if degrees == 0:
            return 0
        if (degrees < 0):
            a = [0, 0, 0]
            coeff = [0,
                    0.394501280250 * 10 ** -1,
                    0.236223735980 * 10 ** -4,
                    -0.328589067840 * 10 ** -6,
                    -0.499048287770 * 10 ** -8,
                    -0.675090591730 * 10 ** -10,
                    -0.574103274280 * 10 ** -12,
                    -0.310888728940 * 10 ** -14,
                    -0.104516093650 * 10 ** -16,
                    -0.198892668780 * 10 ** -19,
                    -0.163226974860 * 10 ** -22]

        else:
            a = [0.1185976, -0.0001183432, 126.9686]
            coeff = [-0.176004136860 * 10 ** -1,
                      0.389212049750 * 10 ** -1,
                      0.185587700320 * 10 ** -4,
                     -0.994575928740 * 10 ** -7,
                      0.318409457190 * 10 ** -9,
                     -0.560728448890 * 10 ** -12,
                      0.560750590590 * 10 ** -15,
                     -0.320207200030 * 10 ** -18,
                      0.971511471520 * 10 ** -22,
                     -0.121047212750 * 10 ** -25]
            
      
        voltage = 0
        for i, c in enumerate(coeff):
            voltage += c * degrees ** i
        voltage += a[0] * exp(a[1] * (degrees - a[2])**2)
        
        return voltage
    
    def _TC_K_VtoC(self, voltage):
        '''
        Function: _TC_K_VtoC
        
        Description: 
            Converts from a voltage to degrees Celsius for a K-type thermocouple.
        
        Returns:
            Float
        '''
        # Coefficients and equations are from NIST:
        #    http://srdata.nist.gov/its90/download/type_k.tab
        # This is only for K-type thermocouples
        if voltage == 0:
            return 0
        if voltage < 0:
            coeff = [0,
                    25.173462,
                    - 1.1662878,
                    - 1.0833638,
                    - 0.8977354,
                    - 0.37342377,
                    - 0.086632643,
                    - 0.010450598,
                    - 0.0005192058]
        elif voltage < 20.644:    #this value is mV! corresponds to 500C
            coeff = [0,
                    25.08355,
                    0.07860106,
                    - 0.2503131,
                    0.0831527,
                    - 0.01228034,
                    0.0009804036,
                    - 0.0000441303,
                    0.0000010577,
                    - 0.0000000105]
        else:
            coeff = [-131.8058,
                    48.30222,
                    - 1.646031,
                    0.05464731,
                    - 0.0009650715,
                    0.0000088022,
                    - 0.0000000311]
       
        degrees = 0
        for i, c in enumerate(coeff):
            degrees += c * voltage ** i  
        
     
        return degrees
    
    def _TC_T_CtoV(self, degrees):
        '''
        Function: _TC_K_CtoV
        
        Description:
            Converts from degrees Celsius to a voltage for a K-type thermocouple.
            
        Returns:
            Float
        '''
        
        #Coefficients and equations are from NIST:
        #   http://srdata.nist.gov/its90/download/type_t.tab
        #These are for T-type thermocouples --
        if degrees == 0:
            return 0
        if (degrees < 0):
            coeff = [0,
                     0.387481063640 * 10 ** -1,
                     0.441944343470 * 10 * -4,
                     0.118443231050 * 10 ** -6,
                     0.200329735540 * 10 ** -7,
                     0.901380195590 * 10 ** -9,
                     0.226511565930 * 10 ** -10,
                     0.360711542050 * 10 ** -12,
                     0.384939398830 * 10 ** -14,
                     0.282135219250 * 10 ** -16,
                     0.142515947790 * 10 ** -18,
                     0.487686622860 * 10 ** -21,
                     0.107955392700 * 10 ** -23,
                     0.139450270620 * 10 ** -26,
                     0.797951539270 * 10 ** -30 ]
        else:
            coeff = [0,
                     0.387481063640 * 10 ** -1,
                     0.332922278800 * 10 ** -4,
                     0.206182434040 * 10 ** -6,
                     - 0.218822568460 * 10 ** -8,
                     0.109968809280 * 10 ** -10,
                     - 0.308157587720 * 10 ** -13,
                     0.454791352900 * 10 ** -16,
                     - 0.275129016730 * 10 ** -19]
                     
       
        voltage = 0
        for i, c in enumerate(coeff):
            voltage += c * degrees ** i
        return voltage
    
    def _TC_T_VtoC(self, voltage):
        '''
        Function: _TC_T_VtoC
        
        Description: 
            Converts from a voltage to degrees Celsius for a T-type thermocouple.
        
        Returns:
            Float
        '''

        # Coefficients and equations are from NIST:
        #    http://srdata.nist.gov/its90/download/type_t.tab
        # This is only for T-type thermocouples 
        if voltage == 0:
            return 0
        if voltage < 0:
            coeff = [0,
                    2.5949192 * 10 ** 1,
                    - 2.1316967 * 10 ** -1,
                    7.9018692 * 10 ** -1,
                    4.2527777 * 10 ** -1,
                    1.3304473 * 10 ** -1,
                    2.0241446 * 10 ** -2,
                    1.2668171 * 10 ** -3]
        else:    
            coeff = [0,
            2.592800 * 10 ** 1,
            - 7.602961 * 10 ** -1,
            4.637791 * 10 ** -2,
            - 2.165394 * 10 ** -3,
            6.048144 * 10 ** -5,
            - 7.293422 * 10 ** -7]
       
        
        degrees = 0
        for i, c in enumerate(coeff):
            degrees += c * voltage ** i   
        return degrees

    
    def _calculate_temperature(self, linearReading, thermocoupleReading, offsetReading):
        '''
        Function: _calculate_temperature
        
        Description:
            Takes in three readings: reference temperature, thermocouple voltage, and offset voltage.  Uses them
            to calculate the temperature at the thermocouple in degrees Celsius.
        
        Parameters:
            Float: linearReading:  the reference sensor voltage
            Float: thermocoupleReading: the voltage at the thermocouple
            FLoat: offestReading: the voltage across the offset potentiometer
        
        Returns:
            Float:  Temperature in degrees Celsius
        '''
        # Ideally, this offset is 2V. Either we use a trimpot to make it exact,
        #  or we change this value in the GUI during calibration
        
        #or...
        #print "linear:", linearReading
        #print "thermo:", thermocoupleReading
        #print "offset:", offsetReading
        
        TC_Offset = offsetReading * OSEKitsDefine.ADC_voltage_max/OSEKitsDefine.ADC_number_of_steps
        
        #print "TC_OFFSET:", TC_Offset
        # Read linear temperature and converts to deg C
        coldJunction = 100*linearReading * OSEKitsDefine.ADC_voltage_max/OSEKitsDefine.ADC_number_of_steps
        
        # convert to voltage
        #print "coldjunction:", coldJunction
        coldJunction = self._TC_K_CtoV(coldJunction) #* (1000/ float(self.gain_value.get()))
        #print "TC_K_CtoV", coldJunction
        
        # Read thermocouple voltage, subtract voltage offset, add cold junction
        #print "gain:", float(self.gain_value.get())
        #print "1000 / gain", (1000 / float(self.gain_value.get()))
        #print "1.1 / 1024", (1.1 / 1024)
        
        thermoTemp = (thermocoupleReading * OSEKitsDefine.ADC_voltage_max/OSEKitsDefine.ADC_number_of_steps) *(OSEKitsDefine.Thermocouple_gain_factor / float(self.gain_value.get()))
        #print "thermotemp 1:", thermoTemp
        thermoTemp = (thermoTemp) + coldJunction - (TC_Offset)
        #print "Thermotemp2", thermoTemp
        #convert to celsius
        #print "TEMPERATURE:",self._TC_K_VtoC(thermoTemp)
        #print "\n"
        return self._TC_K_VtoC(thermoTemp)
    
    
    def start_measurement(self):
        '''
        Function: start_measurement
        
        Description:
            Called when starting to take measurements from the micro-controller. Initializes measurement storage
            and sets appropriate flags and counters.
            
        Returns:
            string  "success" when no errors encountered
                    "<error message>" otherwise
        '''
        
        self._measured_temperatures = []
        self._measured_times = []
        
        
        self._start_time = time.time()
        return "success"
    def stop_measurement(self):
        '''
        Function: stop_measurement
        
        Description:
            Called when done taking measurements from the micro-controller.
            
        Returns:
            string  "success" when no errors encountered
                    "<error message>" otherwise
        '''        
        return
         
    def add_measurement(self, measurement_packet):
        '''
        Function: add_measurement
        
        Description: 
            Takes in a measurement packet and converts the packet into usuable data. Performs
            calculations on the data to get the temperature reading and updates the graph
            display.
        
        Parameters: 
            string  a string containing all characters sent from the micro-controller
            
        Returns:
            string  "success" if no errors encountered
                    "<error message>" otherwise
        '''
        
        shift = 0
        measurement_data = 0
       
        for char in measurement_packet:
            measurement_data = measurement_data | ord(char) << 8 * shift
            shift = shift + 1
        
        linearReading = measurement_data & 0x03FF
        thermocoupleReading = (measurement_data & 0xFFC00) >> 10 
        offsetReading = (measurement_data & 0x3FF00000) >> 20
        
        if len(self._measured_temperatures) > 1:
            self._measured_temperatures.append((self._measured_temperatures[-1] + self._calculate_temperature(linearReading, thermocoupleReading, offsetReading)) / 2)
        else:
            self._measured_temperatures.append(self._calculate_temperature(linearReading, thermocoupleReading, offsetReading))
        
        self._measured_times.append(time.time() - self._start_time)
        
        self._temperature_figure.clear()
        self._temperature_figure.add_subplot(self._temperature_plot)
        self._temperature_plot.plot(self._measured_times,self._measured_temperatures)
        self._temperature_plot.set_title('Temperature vs Time')
        self._temperature_plot.set_xlabel('Time (s)')
        self._temperature_plot.set_ylabel('Temperature (C)')
        self._temperature_figure.subplots_adjust(top=0.9, bottom = 0.125, right = 0.9, left = 0.125)
        self.temperature_display.show()
        return "success" 
